Skip to content

fix(rest): resolve public forms from flattened view metadata#1989

Merged
os-zhuang merged 1 commit into
mainfrom
feat/anonymous-entry-runtime
Jun 16, 2026
Merged

fix(rest): resolve public forms from flattened view metadata#1989
os-zhuang merged 1 commit into
mainfrom
feat/anonymous-entry-runtime

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What

Fix findPublicFormView so the public-form endpoints (/forms/:slug, /forms/:slug/submit) actually resolve standard form views.

Bug

The resolver only matched the authoring view shape (view.form.sharing, view.formViews.{key}.sharing). But getMetaItems({type:'view'}) returns the registered/flattened shape — one item per view: { name: 'crm_lead.web_to_lead', object: 'crm_lead', viewKind: 'form', config: { data, sections, sharing } }. The sharing (allowAnonymous + publicLink) sits under config, which the resolver never inspected.

Result: every standard Web-to-Lead / Web-to-Case form returned FORM_NOT_FOUND — the capability was fully declared (profile + view + spec + UI component) but non-functional at runtime. An unenforced/untested surface.

Fix

Add a third candidate: a viewKind === 'form' item whose config carries sharing. Object name resolves via config.data.object (or the item's object).

Verification (HotCRM, end-to-end)

  • GET /api/v1/forms/contact-us (no auth) → 200 form spec
  • POST /api/v1/forms/contact-us/submit (no auth) → creates a crm_lead
  • POST /api/v1/forms/support/submit (no auth) → creates a crm_case
  • GET /api/v1/data/crm_lead (no auth) → 401 (guests still can't read — INSERT-only enforced)

@objectstack/rest 121/121 tests pass.

Found by browser-verifying web-to-lead. Note: a separate app-side bug also blocked it — guest profiles must key the full object name (crm_lead, not lead) for the anonymous permission path; the short-name path that works for authenticated profiles does not resolve here (possible follow-up: normalize short→full names consistently in the explicit-permission-set path).

🤖 Generated with Claude Code

`findPublicFormView` (the /forms/:slug + /forms/:slug/submit resolver) only
matched the *authoring* view shape — `view.form.sharing` and
`view.formViews.{key}.sharing`. But `getMetaItems({type:'view'})` returns the
*registered* shape: one flattened item per view, `{ name, object, viewKind,
config: { data, sections, sharing } }`. The form's `sharing` (allowAnonymous +
publicLink) lives under `config`, which the resolver never inspected — so every
standard public form silently failed with FORM_NOT_FOUND, making Web-to-Lead /
Web-to-Case non-functional despite being fully declared.

Add a third candidate source: a `viewKind === 'form'` item whose `config` carries
`sharing`. Object name resolves via `config.data.object` / the item's `object`.

Verified end-to-end (HotCRM): anonymous GET /api/v1/forms/contact-us → 200 form
spec; POST /api/v1/forms/contact-us/submit (no auth) → creates a crm_lead;
/forms/support → crm_case; guest reads still 401 (INSERT-only enforced).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@os-zhuang
os-zhuang merged commit 768331d into main Jun 16, 2026
1 of 2 checks passed
@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jun 16, 2026 10:36pm

Request Review

@os-zhuang
os-zhuang deleted the feat/anonymous-entry-runtime branch June 16, 2026 22:36
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest.

5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/implementation-status.mdx (via @objectstack/rest)
  • content/docs/concepts/packages.mdx (via @objectstack/rest)
  • content/docs/guides/api-reference.mdx (via @objectstack/rest)
  • content/docs/guides/packages.mdx (via @objectstack/rest)
  • content/docs/guides/plugins.mdx (via @objectstack/rest)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

os-zhuang added a commit that referenced this pull request Jun 17, 2026
The app-crm example showed Web-to-Lead only via `customer.portal` `anonymousEntry`
— a spec property with NO runtime consumer, so it never worked (404). Add the
mechanism that actually does, and make the example coherent:

- lead.view.ts: `web_to_lead` form view with `sharing.allowAnonymous` →
  live `GET/POST /api/v1/forms/contact-us` (+ /submit).
- sales-roles.ts: `GuestPortalProfile` (isProfile, INSERT-only on crm_lead, keyed
  by FULL object name — the anonymous permission path requires it).
- lead.object.ts: `status` gets `defaultValue: 'new'` so a minimal public create
  satisfies `required` (the option-level `default` is only a UI preselect).
- customer.portal.ts: drop the dead `anonymousEntry` routes; point to the working
  form view instead (re-add when the runtime mounts anonymousEntry).
- security/index.ts + objectstack.config.ts: export + register the guest profile.

Doubles as a CI regression test for the public-form path (builds against the
workspace) — the #1989 flattened-metadata resolution bug would have been caught here.

Verified end-to-end (app-crm, no auth): GET /forms/contact-us → 200 form spec;
POST /forms/contact-us/submit → creates a crm_lead (status=new); GET /data/crm_lead
→ 401 (guests can't read).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jun 17, 2026
…bjects (#1997)

* feat(crm): working Web-to-Lead public form example

The app-crm example showed Web-to-Lead only via `customer.portal` `anonymousEntry`
— a spec property with NO runtime consumer, so it never worked (404). Add the
mechanism that actually does, and make the example coherent:

- lead.view.ts: `web_to_lead` form view with `sharing.allowAnonymous` →
  live `GET/POST /api/v1/forms/contact-us` (+ /submit).
- sales-roles.ts: `GuestPortalProfile` (isProfile, INSERT-only on crm_lead, keyed
  by FULL object name — the anonymous permission path requires it).
- lead.object.ts: `status` gets `defaultValue: 'new'` so a minimal public create
  satisfies `required` (the option-level `default` is only a UI preselect).
- customer.portal.ts: drop the dead `anonymousEntry` routes; point to the working
  form view instead (re-add when the runtime mounts anonymousEntry).
- security/index.ts + objectstack.config.ts: export + register the guest profile.

Doubles as a CI regression test for the public-form path (builds against the
workspace) — the #1989 flattened-metadata resolution bug would have been caught here.

Verified end-to-end (app-crm, no auth): GET /forms/contact-us → 200 form spec;
POST /forms/contact-us/submit → creates a crm_lead (status=new); GET /data/crm_lead
→ 401 (guests can't read).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spec): cross-validate permission/profile object grants against declared objects

`validateCrossReferences` checked hook/view/seed/app-nav/action references against
the declared object set — but NOT permission-set/profile `objects` grants, even
though `validateNamespacePrefix`'s doc already assumed it did. So a profile that
grants on a non-existent object (e.g. a short `lead` instead of the namespaced
`crm_lead`) passed build/validate/test silently. The grant then applies to
nothing: the authenticated path may namespace-resolve the short name, but the
anonymous / explicit-permission-set path does not — so e.g. a public Web-to-Lead
INSERT is denied for "roles []", with no diagnostic anywhere.

Add the missing check: every `permissions[].objects` key must reference a declared
object, or strict validation fails loudly at build time.

- stack.zod.ts: validate permission/profile object grants → object references
- stack.test.ts: +2 cases (short/undefined name fails; full name passes)

82/82 spec stack tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant